lib:
{
  # git tag
  k3sVersion,
  # commit hash
  k3sCommit,
  k3sRepoSha256 ? lib.fakeHash,
  k3sVendorHash ? lib.fakeHash,
  # taken from ./scripts/version.sh VERSION_ROOT https://github.com/k3s-io/k3s/blob/v1.23.3%2Bk3s1/scripts/version.sh#L47
  k3sRootVersion,
  k3sRootSha256 ? lib.fakeHash,
  # Based on the traefik charts here: https://github.com/k3s-io/k3s/blob/d71ab6317e22dd34673faa307a412a37a16767f6/scripts/download#L29-L32
  # see also https://github.com/k3s-io/k3s/blob/d71ab6317e22dd34673faa307a412a37a16767f6/manifests/traefik.yaml#L8
  chartVersions,
  # Air gap container images that are released as assets with every k3s release
  imagesVersions,
  # taken from ./scripts/version.sh VERSION_CNIPLUGINS https://github.com/k3s-io/k3s/blob/v1.23.3%2Bk3s1/scripts/version.sh#L45
  k3sCNIVersion,
  k3sCNISha256 ? lib.fakeHash,
  # taken from ./scripts/version.sh VERSION_CONTAINERD
  containerdVersion,
  containerdSha256 ? lib.fakeHash,
  # run `grep github.com/kubernetes-sigs/cri-tools go.mod | head -n1 | awk '{print $4}'` in the k3s repo at the tag
  criCtlVersion,
  updateScript ? null,
}@attrs:

# builder.nix contains a "builder" expression that, given k3s version and hash
# variables, creates a package for that version.
# Due to variance in k3s's build process, this builder only works for k3s 1.26+
# currently.
# It is likely we will have to split out additional builders for additional
# versions in the future, or customize this one further.
{
  bash,
  bridge-utils,
  btrfs-progs,
  buildGoModule,
  conntrack-tools,
  coreutils,
  ethtool,
  fetchFromGitHub,
  fetchgit,
  fetchurl,
  fetchzip,
  findutils,
  gnugrep,
  gnused,
  go,
  iproute2,
  ipset,
  iptables,
  nftables,
  kmod,
  lib,
  libseccomp,
  makeBinaryWrapper,
  nixosTests,
  overrideBundleAttrs ? { }, # An attrSet/function to override the `k3sBundle` derivation.
  overrideCniPluginsAttrs ? { }, # An attrSet/function to override the `k3sCNIPlugins` derivation.
  overrideContainerdAttrs ? { }, # An attrSet/function to override the `k3sContainerd` derivation.
  pkg-config,
  pkgsBuildBuild,
  procps,
  rsync,
  runCommand,
  runc,
  socat,
  sqlite,
  stdenv,
  shadow,
  systemdMinimal,
  util-linuxMinimal,
  yq-go,
  zstd,
  versionCheckHook,
}:

# k3s is a kinda weird derivation. One of the main points of k3s is the
# simplicity of it being one binary that can perform several tasks.
# However, when you have a good package manager (like nix), that doesn't
# actually make much of a difference; you don't really care if it's one binary
# or 10 since with a good package manager, installing and running it is
# identical.
# Since upstream k3s packages itself as one large binary with several
# "personalities" (in the form of subcommands like 'k3s agent' and 'k3s
# kubectl'), it ends up being easiest to mostly mimic upstream packaging, with
# some exceptions.
# K3s also carries patches to some packages (such as containerd and cni
# plugins), so we intentionally use the k3s versions of those binaries for k3s,
# even if the upstream version of those binaries exist in nixpkgs already. In
# the end, that means we have a thick k3s binary that behaves like the upstream
# one for the most part.
# However, k3s also bundles several pieces of unpatched software, from the
# strongswan vpn software, to iptables, to socat, conntrack, busybox, etc.
# Those pieces of software we entirely ignore upstream's handling of, and just
# make sure they're in the path if desired.
let

  baseMeta = {
    description = "Lightweight Kubernetes distribution";
    license = lib.licenses.asl20;
    homepage = "https://k3s.io";
    teams = [ lib.teams.k3s ];
    platforms = lib.platforms.linux;

    # resolves collisions with other installations of kubectl, crictl, ctr
    # prefer non-k3s versions
    priority = 5;
  };

  # https://github.com/k3s-io/k3s/blob/5fb370e53e0014dc96183b8ecb2c25a61e891e76/scripts/build#L19-L40
  versionldflags = [
    "-X github.com/k3s-io/k3s/pkg/version.Version=v${k3sVersion}"
    "-X github.com/k3s-io/k3s/pkg/version.GitCommit=${lib.substring 0 8 k3sCommit}"
    "-X github.com/k3s-io/k3s/pkg/version.UpstreamGolang=go${go.version}"
    "-X k8s.io/client-go/pkg/version.gitVersion=v${k3sVersion}"
    "-X k8s.io/client-go/pkg/version.gitCommit=${k3sCommit}"
    "-X k8s.io/client-go/pkg/version.gitTreeState=clean"
    "-X k8s.io/client-go/pkg/version.buildDate=1970-01-01T01:01:01Z"
    "-X k8s.io/component-base/version.gitVersion=v${k3sVersion}"
    "-X k8s.io/component-base/version.gitCommit=${k3sCommit}"
    "-X k8s.io/component-base/version.gitTreeState=clean"
    "-X k8s.io/component-base/version.buildDate=1970-01-01T01:01:01Z"
    "-X github.com/kubernetes-sigs/cri-tools/pkg/version.Version=v${criCtlVersion}"
    "-X github.com/containerd/containerd/version.Version=v${containerdVersion}"
    "-X github.com/containerd/containerd/version.Package=github.com/k3s-io/containerd"
  ];

  # bundled into the k3s binary
  traefik = {
    chart = fetchurl chartVersions.traefik;
    name = baseNameOf chartVersions.traefik.url;
  };
  traefik-crd = {
    chart = fetchurl chartVersions.traefik-crd;
    name = baseNameOf chartVersions.traefik-crd.url;
  };

  # a shortcut that provides the images archive for the host platform. Currently only supports
  # aarch64 (arm64) and x86_64 (amd64), throws on other architectures.
  airgap-images =
    {
      x86_64-linux = fetchurl imagesVersions.airgap-images-amd64-tar-zst;
      aarch64-linux = fetchurl imagesVersions.airgap-images-arm64-tar-zst;
    }
    .${stdenv.hostPlatform.system}
      or (throw "k3s: no airgap images available for system ${stdenv.hostPlatform.system}, consider using an image archive with an explicit architecture.");

  # so, k3s is a complicated thing to package
  # This derivation attempts to avoid including any random binaries from the
  # internet. k3s-root is _mostly_ binaries built to be bundled in k3s (which
  # we don't care about doing, we can add those as build or runtime
  # dependencies using a real package manager).
  # In addition to those binaries, it's also configuration though (right now
  # mostly strongswan configuration), and k3s does use those files.
  # As such, we download it in order to grab 'etc' and bundle it into the final
  # k3s binary.
  k3sRoot = fetchzip {
    # Note: marked as apache 2.0 license
    url = "https://github.com/k3s-io/k3s-root/releases/download/v${k3sRootVersion}/k3s-root-amd64.tar";
    sha256 = k3sRootSha256;
    stripRoot = false;
  };
  k3sCNIPlugins =
    (buildGoModule rec {
      pname = "k3s-cni-plugins";
      version = k3sCNIVersion;
      vendorHash = null;

      subPackages = [ "." ];

      src = fetchFromGitHub {
        owner = "rancher";
        repo = "plugins";
        rev = "v${version}";
        sha256 = k3sCNISha256;
      };

      postInstall = ''
        mv $out/bin/plugins $out/bin/cni
      '';

      meta = baseMeta // {
        description = "CNI plugins, as patched by rancher for k3s";
      };
    }).overrideAttrs
      overrideCniPluginsAttrs;
  # Grab this separately from a build because it's used by both stages of the
  # k3s build.
  k3sRepo = fetchgit {
    url = "https://github.com/k3s-io/k3s";
    rev = "v${k3sVersion}";
    sha256 = k3sRepoSha256;
  };

  # Modify the k3s installer script so that we can let it install only
  # killall.sh
  k3sKillallSh = runCommand "k3s-killall.sh" { } ''
    # Copy the upstream k3s install script except for the last lines that
    # actually run the install process
    sed --quiet '/# --- run the install process --/q;p' ${k3sRepo}/install.sh > install.sh

    # Let killall expect "containerd-shim" in the Nix store
    substituteInPlace install.sh \
      --replace-fail '"''${K3S_DATA_DIR}"' "" \
      --replace-fail '/data/[^/]*/bin/containerd-shim' \
        '/nix/store/[^/]*k3s-containerd[^/]*/bin/containerd-shim'

    remove_matching_line() {
      line_to_delete=$(grep -n "$1" install.sh | cut -d : -f 1 || true)
      if [ -z $line_to_delete ]; then
        echo "failed to find expression \"$1\" in k3s installer script (install.sh)"
        exit 1
      fi
      sed -i "''${line_to_delete}d" install.sh
    }

    # Don't change mode and owner of killall
    remove_matching_line "chmod.*KILLALL_K3S_SH"
    remove_matching_line "chown.*KILLALL_K3S_SH"

    # Execute only the "create_killall" function of the installer script
    sed -i '$acreate_killall' install.sh

    KILLALL_K3S_SH=$out bash install.sh
  '';

  # Stage 1 of the k3s build:
  # Let's talk about how k3s is structured.
  # One of the ideas of k3s is that there's the single "k3s" binary which can
  # do everything you need, from running a k3s server, to being a worker node,
  # to running kubectl.
  # The way that actually works is that k3s is a single go binary that contains
  # a bunch of bindata that it unpacks at runtime into directories (either the
  # user's home directory or /var/lib/rancher if run as root).
  # This bindata includes both binaries and configuration.
  # In order to let nixpkgs do all its autostripping/patching/etc, we split this into two derivations.
  # First, we build all the binaries that get packed into the thick k3s binary
  # (and output them from one derivation so they'll all be suitably patched up).
  # Then, we bundle those binaries into our thick k3s binary and use that as
  # the final single output.
  # This approach was chosen because it ensures the bundled binaries all are
  # correctly built to run with nix (we can lean on the existing buildGoModule
  # stuff), and we can again lean on that tooling for the final k3s binary too.
  # Other alternatives would be to manually run the
  # strip/patchelf/remove-references step ourselves in the installPhase of the
  # derivation when we've built all the binaries, but haven't bundled them in
  # with generated bindata yet.

  k3sBundle =
    (buildGoModule {
      pname = "k3s-bin";
      version = k3sVersion;

      src = k3sRepo;
      vendorHash = k3sVendorHash;

      nativeBuildInputs = [ pkg-config ];
      buildInputs = [
        libseccomp
        sqlite.dev
      ];

      subPackages = [ "cmd/server" ];
      ldflags = versionldflags;

      tags = [
        "ctrd"
        "libsqlite3"
        "linux"
      ];

      # Set flags for sqlite dbstat
      CGO_CFLAGS = "-DSQLITE_ENABLE_DBSTAT_VTAB=1 -DSQLITE_USE_ALLOCA=1";

      # Copy manifests and static charts pre build so they get embedded during build
      preBuild = ''
        cp -av manifests/* ./pkg/deploy/embed/

        mkdir -p ./pkg/static/embed/charts/
        cp -v ${traefik.chart} ./pkg/static/embed/charts/${traefik.name}
        cp -v ${traefik-crd.chart} ./pkg/static/embed/charts/${traefik-crd.name}
      '';

      # create the multicall symlinks for k3s
      postInstall = ''
        mv $out/bin/server $out/bin/k3s
        pushd $out
        # taken verbatim from https://github.com/k3s-io/k3s/blob/v1.23.3%2Bk3s1/scripts/build#L105-L113
        ln -s k3s ./bin/containerd
        ln -s k3s ./bin/crictl
        ln -s k3s ./bin/ctr
        ln -s k3s ./bin/k3s-agent
        ln -s k3s ./bin/k3s-certificate
        ln -s k3s ./bin/k3s-completion
        ln -s k3s ./bin/k3s-etcd-snapshot
        ln -s k3s ./bin/k3s-secrets-encrypt
        ln -s k3s ./bin/k3s-server
        ln -s k3s ./bin/k3s-token
        ln -s k3s ./bin/kubectl
        popd
      '';

      meta = baseMeta // {
        description = "Various binaries that get packaged into the final k3s binary";
      };
    }).overrideAttrs
      overrideBundleAttrs;
  # Only used for the shim since
  # https://github.com/k3s-io/k3s/blob/v1.27.2%2Bk3s1/scripts/build#L153
  k3sContainerd =
    (buildGoModule {
      pname = "k3s-containerd";
      version = containerdVersion;
      src = fetchFromGitHub {
        owner = "k3s-io";
        repo = "containerd";
        rev = "v${containerdVersion}";
        sha256 = containerdSha256;
      };
      vendorHash = null;
      buildInputs = [ btrfs-progs ];
      subPackages = [ "cmd/containerd-shim-runc-v2" ];
      ldflags = versionldflags;
    }).overrideAttrs
      overrideContainerdAttrs;
in
buildGoModule (finalAttrs: {
  pname = "k3s";
  version = k3sVersion;
  pos = builtins.unsafeGetAttrPos "k3sVersion" attrs;

  tags = [
    "libsqlite3"
    "linux"
    "ctrd"
  ];
  src = k3sRepo;
  vendorHash = k3sVendorHash;

  postPatch = ''
    # Nix prefers dynamically linked binaries over static binary.

    substituteInPlace scripts/package-cli \
      --replace-fail '"$LDFLAGS $STATIC" -o' \
                '"$LDFLAGS" -o'

    # Upstream codegen fails with trimpath set. Removes "trimpath" for 'go generate':

    substituteInPlace scripts/package-cli \
      --replace-fail '"''${GO}" generate' \
                'GOFLAGS="" \
                 GOOS="${pkgsBuildBuild.go.GOOS}" \
                 GOARCH="${pkgsBuildBuild.go.GOARCH}" \
                 CC="${pkgsBuildBuild.stdenv.cc}/bin/cc" \
                 "''${GO}" generate'

    # Add the -e flag to process "errornous" packages. We need to modify this because the upstream
    # build-time version detection doesn't work with a vendor directory.
    substituteInPlace scripts/version.sh \
      --replace-fail \
        "go list -mod=readonly -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' \$1" \
        "go list -mod=readonly -e -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' \$1"
  '';

  # Important utilities used by the kubelet, see
  # https://github.com/kubernetes/kubernetes/issues/26093#issuecomment-237202494
  # Note the list in that issue is stale and some aren't relevant for k3s.
  k3sRuntimeDeps = [
    kmod
    socat
    iptables
    nftables
    iproute2
    ipset
    bridge-utils
    ethtool
    util-linuxMinimal # kubelet wants 'nsenter' and 'mount' from util-linux: https://github.com/kubernetes/kubernetes/issues/26093#issuecomment-705994388
    conntrack-tools
    runc
    bash
    shadow # kubelet wants 'getsubids' when using user namespaces
  ];

  k3sKillallDeps = [
    bash
    systemdMinimal
    procps
    coreutils
    gnugrep
    findutils
    gnused
  ];

  buildInputs = finalAttrs.k3sRuntimeDeps;

  nativeBuildInputs = [
    makeBinaryWrapper
    rsync
    yq-go
    zstd
  ];

  # embedded in the final k3s cli
  propagatedBuildInputs = [
    k3sCNIPlugins
    k3sContainerd
    k3sBundle
  ];

  # We override most of buildPhase due to peculiarities in k3s's build.
  # Specifically, it has a 'go generate' which runs part of the package. See
  # this comment:
  # https://github.com/NixOS/nixpkgs/pull/158089#discussion_r799965694
  # So, why do we use buildGoModule at all? For the `vendorHash` / `go mod download` stuff primarily.
  buildPhase = ''
    runHook preBuild
    patchShebangs ./scripts/package-cli ./scripts/download ./scripts/build-upload

    # copy needed 'go generate' inputs into place
    mkdir -p ./bin/aux
    rsync -a --no-perms ${k3sBundle}/bin/ ./bin/
    ln -vsf ${k3sCNIPlugins}/bin/cni ./bin/cni
    ln -vsf ${k3sContainerd}/bin/containerd-shim-runc-v2 ./bin
    rsync -a --no-perms --chmod u=rwX ${k3sRoot}/etc/ ./etc/

    export ARCH=$GOARCH
    export DRONE_TAG="v${k3sVersion}"
    export DRONE_COMMIT="${k3sCommit}"
    # use ./scripts/package-cli to run 'go generate' + 'go build'

    ./scripts/package-cli
    mkdir -p $out/bin
    runHook postBuild
  '';

  # Otherwise it depends on 'getGoDirs', which is normally set in buildPhase
  doCheck = false;

  installPhase = ''
    runHook preInstall
    # wildcard to match the arm64 build too
    install -m 0755 dist/artifacts/k3s* -D $out/bin/k3s
    wrapProgram $out/bin/k3s \
      --prefix PATH : ${lib.makeBinPath finalAttrs.k3sRuntimeDeps} \
      --prefix PATH : "$out/bin"
    ln -s $out/bin/k3s $out/bin/kubectl
    ln -s $out/bin/k3s $out/bin/crictl
    ln -s $out/bin/k3s $out/bin/ctr
    install -m 0755 ${k3sKillallSh} -D $out/bin/k3s-killall.sh
    wrapProgram $out/bin/k3s-killall.sh \
      --prefix PATH : ${lib.makeBinPath (finalAttrs.k3sRuntimeDeps ++ finalAttrs.k3sKillallDeps)}
    runHook postInstall
  '';

  doInstallCheck = true;
  nativeInstallCheckInputs = [ versionCheckHook ];
  versionCheckProgramArg = "--version";

  passthru = {
    inherit
      airgap-images
      k3sCNIPlugins
      k3sContainerd
      k3sRepo
      k3sRoot
      k3sBundle
      updateScript
      ;
    tests =
      let
        mkTests =
          version:
          let
            k3s_version = "k3s_" + lib.replaceStrings [ "." ] [ "_" ] (lib.versions.majorMinor version);
          in
          lib.mapAttrs (name: value: nixosTests.k3s.${name}.${k3s_version}) nixosTests.k3s;
      in
      mkTests k3sVersion;
    imagesList = throw "k3s.imagesList was removed";
    airgapImages = throw "k3s.airgapImages was renamed to k3s.airgap-images";
    airgapImagesAmd64 = throw "k3s.airgapImagesAmd64 was renamed to k3s.airgap-images-amd64-tar-zst";
    airgapImagesArm64 = throw "k3s.airgapImagesArm64 was renamed to k3s.airgap-images-arm64-tar-zst";
    airgapImagesArm = throw "k3s.airgapImagesArm was renamed to k3s.airgap-images-arm-tar-zst";
  }
  // (lib.mapAttrs (_: value: fetchurl value) imagesVersions);

  meta = baseMeta // {
    mainProgram = "k3s";
  };
})
